package com.corewillsoft.fireflies.engine;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import com.corewillsoft.fireflies.gamelaws.BaseLaw;
import com.corewillsoft.fireflies.gamelaws.LawFactory;
import com.corewillsoft.fireflies.gamelaws.LawType;
import com.corewillsoft.fireflies.gameobjects.Firefly;
import com.corewillsoft.fireflies.gameobjects.GameObject;
import com.corewillsoft.fireflies.gameobjects.Portal;
import com.corewillsoft.fireflies.gameobjects.fireflyraces.RaceType;
import com.corewillsoft.fireflies.gameobjects.obstacles.Obstacle;
import com.corewillsoft.fireflies.gameobjects.obstacles.Obstacle.ObstacleType;
import com.corewillsoft.fireflies.gameobjects.obstacles.ObstacleFactory;
import com.corewillsoft.fireflies.storage.DbManager;
import com.corewillsoft.fireflies.storage.objects.BaseGameLevel;
import com.corewillsoft.fireflies.storage.objects.SavedGameLevel;
import com.corewillsoft.fireflies.storage.objects.StartGameLevel;
import com.corewillsoft.fireflies.util.DebugLog;

public class GameEngine {

	private static final int THREAD_POOL_SIZE = 3;
	private static final int GAME_TICK_MILLIS = 100;
	private static final LawType[] COMMON_LAW_TYPE = {
			LawType.GRAVITY,
			LawType.INERTIA,
			LawType.UPDATE
	};

	private static GameEngine instance;

	private final Set<Firefly> fireflies = Collections.synchronizedSet(new HashSet<Firefly>());
	private final Set<Portal> portals = Collections.synchronizedSet(new HashSet<Portal>());
	private final Set<Obstacle> obstacles = Collections.synchronizedSet(new HashSet<Obstacle>());
	private final Set<BaseLaw> laws = Collections.synchronizedSet(new HashSet<BaseLaw>());
	private final AtomicBoolean finished = new AtomicBoolean(false);
	private final Map<BaseLaw, ScheduledFuture<?>> scheduledTasks;

	private Set<GameObject> gameObjects;
	private ScheduledExecutorService executor;
	private IGameEngine listener;
	private final DbManager dbManager;
	private final BaseGameLevel gameLevel;
	private boolean isGameRunning = true;

	/**
	 * @param level
	 *            level number
	 * @param newGame
	 *            flag which indicate kind of gameLevel. if @true, will be retrieved saved data.
	 */
	private GameEngine(float level, boolean newGame) {
		dbManager = new DbManager();

		gameLevel = dbManager.getGameLevel(level, newGame);
		
		if (gameLevel instanceof StartGameLevel) {
			DebugLog.d("GameEngine StartGameLevel");
		}

		if (gameLevel instanceof SavedGameLevel) {
			DebugLog.d("GameEngine SavedGameLevel");
			
		}
		initFireflies(gameLevel);
		initPortals(gameLevel);
		initObstacles(gameLevel);

		initCommonLaws();
		initLaws();

		scheduledTasks = new HashMap<BaseLaw, ScheduledFuture<?>>();
	}

	private void initLaws() {
		gameObjects = Collections.synchronizedSet(new HashSet<GameObject>());
		gameObjects.addAll(fireflies);
		gameObjects.addAll(portals);
		gameObjects.addAll(obstacles);
		for (GameObject object : gameObjects) {
			laws.addAll(object.getCreateLaws());
		}
	}

	private void initCommonLaws() {
		for (LawType lawType : COMMON_LAW_TYPE) {
			BaseLaw law = LawFactory.getLaw(lawType);
			laws.add(law);
		}
	}

	private void initFireflies(BaseGameLevel gameLevel) {
		List<Integer> ids = gameLevel.getFireflyIds();
		List<Integer> xs = gameLevel.getFireflyX();
		List<Integer> ys = gameLevel.getFireflyY();
		List<Float> energy = gameLevel.getFireflyEnergy();
		List<Boolean> alive = gameLevel.getFireflyAlive();
		List<Boolean> saved = gameLevel.getFireflySaved();

		for (int i = 0; i < ids.size(); i++) {
			RaceType race = RaceType.values()[ids.get(i)];
			Firefly firefly = new Firefly(xs.get(i), ys.get(i), race);
			firefly.setEnergy(energy.get(i));
			firefly.setAlive(alive.get(i));
			firefly.setSaved(saved.get(i));
			fireflies.add(firefly);
		}
	}

	private void initPortals(BaseGameLevel gameLevel) {
		List<Integer> ids = gameLevel.getPortalIds();
		List<Integer> xs = gameLevel.getPortalX();
		List<Integer> ys = gameLevel.getPortalY();

		for (int i = 0; i < ids.size(); i++) {
			RaceType race = RaceType.values()[ids.get(i)];
			Portal portal = new Portal(
					xs.get(i),
					ys.get(i),
					race);
			portals.add(portal);
		}
	}

	private void initObstacles(BaseGameLevel gameLevel) {
		List<Integer> ids = gameLevel.getObstacleIds();
		List<Integer> xs = gameLevel.getObstacleX();
		List<Integer> ys = gameLevel.getObstacleY();
		List<Obstacle.ObstacleParameters> obstaclesParameters = gameLevel.getObstaclesParameters();

		for (int i = 0; i < ids.size(); i++) {
			Obstacle obstacle = ObstacleFactory.createObstacle(
					ObstacleType.values()[ids.get(i)],
					xs.get(i),
					ys.get(i),
					obstaclesParameters.get(i));
			obstacles.add(obstacle);
		}
	}

	/**
	 * save current game level to database.
	 */
	public void saveGameLevel() {
		dbManager.saveIncompletedGameLevel(gameLevel.getLevel(), gameObjects);
	}

	/**
	 * Initializes Engine with the current {@link BaseGameLevel}
	 * @param level
	 *            current game level
	 * @param newGame
	 *            indicate to start game from saved state, by default @false
	 */
	public static void initInstance(float level, boolean newGame) {
		instance = new GameEngine(level, newGame);
	}

	/**
	 * Initializes Engine with the current {@link BaseGameLevel}
	 * @param level
	 *            current game level
	 */
	public static void initInstance(float level) {
		instance = new GameEngine(level, true);
	}

	/**
	 * Gets Engine instance for external use
	 * @return {@link GameEngine} instance for the current level
	 */
	public static GameEngine getInstance() {
		return instance;
	}

	/**
	 * Method called to start {@link Firefly}, {@link Portal} and {@link Obstacle} objects states recalculation
	 */
	public void start() {
		executor = Executors.newScheduledThreadPool(THREAD_POOL_SIZE);
		for (BaseLaw law : laws) {
			addTask(law);
		}
	}

	private void addTask(BaseLaw law) {
		UpdateTask task = new UpdateTask();
		task.setLaw(law);

		// add task delay by law type
		Integer lawTick = law.getType().getLawTick();
		if (lawTick == null) {
			lawTick = GAME_TICK_MILLIS;
		}

		ScheduledFuture<?> scheduledTask = executor.scheduleAtFixedRate(task, 0, lawTick,
				TimeUnit.MILLISECONDS);

		scheduledTasks.put(law, scheduledTask);
	}

	private void removeTask(BaseLaw law) {
		if (scheduledTasks.containsKey(law)) {
			scheduledTasks.get(law).cancel(false);
			scheduledTasks.remove(law);
		}
	}

	/**
	 * Method called to stop {@link Firefly}, {@link Portal} and {@link Obstacle} objects states recalculation
	 */
	public void stop() {
		for (ScheduledFuture<?> task : scheduledTasks.values()) {
			task.cancel(false);
		}
		executor.shutdown();
	}

	public Set<Firefly> getFireflies() {
		return fireflies;
	}

	public Set<Portal> getPortals() {
		return portals;
	}

	public Set<Obstacle> getObstacles() {
		return obstacles;
	}

	private class UpdateTask implements Runnable {

		private BaseLaw law;

		void setLaw(BaseLaw law) {
			this.law = law;
		}

		@Override
		public void run() {
			try {
				applicateLawToGameObjects(law);
				checkResult();
			} catch (Exception e) {
				DebugLog.logException(e);
			}
		}
	}

	// check and apply laws
	private void applicateLawToGameObjects(BaseLaw law) throws Exception {
		for (GameObject gameObject : gameObjects) {

			if (!isGameRunning) {
				continue;
			}

			if (!gameObject.hasSupportedLaws()) {
				continue;
			}

			if (gameObject.isOwnLaw(law)) {
				updateLaws(gameObject);
				continue;
			}

			if (!law.isInArea(gameObject)) {
				continue;
			}

			if (!gameObject.isLawSupported(law.getType())) {
				continue;
			}

			law.applicate(gameObject);
		}
	}

	private void checkResult() {
		if (finished.get()) {
			return;
		}

		for (Firefly firefly : fireflies) {
			if (firefly.isActive()) {
				return;
			}
		}
		finished.set(true);
		levelFinished();
	}

	private void levelFinished() {
		if (listener != null) {
			listener.onLevelFinished();
		}
	}

	public void saveFinishedGameLevel() {
		dbManager.saveCompletedGameLevel(gameLevel.getLevel(), gameObjects);
	}

	public void setListener(IGameEngine listener) {
		this.listener = listener;
	}

	// check for update laws. Reason, gameObject updated his createLaws
	private void updateLaws(GameObject gameObject) throws Exception {
		if (gameObject.isThereNewLaws()) {

			Set<BaseLaw> lawsToDelete = new HashSet<BaseLaw>(laws);
			for (BaseLaw law : lawsToDelete) {
				if (gameObject.isOwnLaw(law)) {
					removeLaw(law);
				}
			}

			Set<BaseLaw> newLaws = gameObject.getCreateLaws();
			for (BaseLaw law : newLaws) {
				addTask(law);
				laws.add(law);
			}
		}
	}

	/**
	 * add law to laws update executor
	 */
	public void addLaw(BaseLaw law) {
		laws.add(law);
		addTask(law);
	}

	/**
	 * remove law from laws update executor
	 */
	public void removeLaw(BaseLaw law) {
		laws.remove(law);
		removeTask(law);
	}

	public void pause() {
		isGameRunning = false;
	}

	public void resume() {
		isGameRunning = true;
	}
}
